# -*-coding:utf-8-*-

import logging, traceback, sys
import pyrestful.rest
from tornado.options import options
import redis
from tornado.ioloop import PeriodicCallback
import time
import paho.mqtt.client as mqtt
from tornado.ioloop import IOLoop, PeriodicCallback
import functools
from bson.objectid import ObjectId
import json
import settings
import urlparse
import base64
from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPClient
from tornado.gen import coroutine, Return
from utils.misc import extract_fields, exclude_fields
import tornado.concurrent
import tornadoredis
from tornado.options import options

_error_codes = {
    # 注意：10000 ~ 10099 错误码分区用于接入联调，error_message 中会给出相应指示信息
    'invalid_request': 10000,  # 无效的请求，一般该错误会导致服务端 throw exception
    'out_of_range': 10001,  # 参数超出范围
    'invalid_path': 10002,  # 非法的 API 路径
    'keys_unsupported': 10003,  # 请求中出现非法字段
    'keys_required_not_present': 10004,  # 要求的字段并没有出现在请求当中

    'invalid_app_key': 10100,
    'sign_expired': 10101,
    'invalid_sign': 10102,
    'permission_denied': 10103,

    'access_token_needed': 10200,
    'access_token_expired': 10201,
    'invalid_access_token': 10202,

    'invalid_sms_code': 10300,  # 无效的短信验证码
    'sms_code_send_fail': 10301,  # 短信验证码发送失败，原因见 error_message
    'invalid_receive_number': 10302,  # 无效的手机号

    'user_already_exists': 10400,
    'user_not_exists': 10401,
    'wrong_password': 10402,
    'user_not_exist_or_wrong_password': 10403,
    'invalid_email_format': 10405,
    'invalid_spreader': 10406,
    'user_id_not_match': 10407,
    'invalid_auth_code': 10408,
    'invalid_roles': 10409,

    'device_already_exists': 10500,
    'invalid_device_id': 10501,
    'device_own_by_other': 10502,
    'not_your_own_device': 10503,

    'mq_request_timeout': 10600,

    'external_service_error': 10700,

    'delegate_error': 10800,
}


class ErrorResponseException(Exception):
    def __init__(self, key, msg=None, error_code=None):
        self.key = key
        self.msg = msg
        self.error_code = error_code


CONNECTION_POOL = tornadoredis.ConnectionPool(max_connections=128,
                                              wait_for_available=True)


class BaseHandler(pyrestful.rest.RestHandler):
    _mqtornado = None
    _redis_client = None

    def __init__(self, application, request, **kwargs):
        super(BaseHandler, self).__init__(application, request, **kwargs)
        self.logger = logging.getLogger()
        self.prod = options.prod

        # 将常用的 daos 放这里以节省资源开销和提高处理速度
        # async daos
        self.apps_dao = self.settings['daos_async']['apps']
        self.roles_dao = self.settings['daos_async']['roles']
        self.devices_dao = self.settings['daos_async']['devices']
        self.users_dao = self.settings['daos_async']['users']
        self.orders_dao = self.settings['daos_async']['orders']
        self.products_dao = self.settings['daos_async']['products']
        self.transfers_dao = self.settings['daos_async']['transfers']
        
    def get_delegate(self, name):
        from settings import DELEGATES
        appkey = self.app.appkey
        if appkey in DELEGATES and name in DELEGATES[appkey]:
            return DELEGATES[appkey][name](handler=self)
        elif '*' in DELEGATES and name in DELEGATES['*']:
            return DELEGATES['*'][name](handler=self)
        else:
            from delegates.base import BaseDelegate
            return BaseDelegate(handler=self)

    @coroutine
    def delegate_get_list(self, dao, id_field_name, get_count=False, **kwargs):
        kwargs.update(self.query_arguments())
        filter_cond, projection = yield self.delegate_pre_get_list(self.delegate_name, **kwargs)
        self.logger.info('-> on pre get list result: %r, %r', filter_cond, projection)
        if get_count:
            count = yield dao.count(filter_cond)
            result = dict(count=count)
        else:
            delegate = self.get_delegate(self.delegate_name)
            result = yield dao.get_paging_result(filter_cond, projection, id_field_name,
                                                 delegate.on_per_list_item, **kwargs)
        raise Return(result)

    @coroutine
    def delegate_pre_get_statistics(self, delegate_name, **kwargs):
        delegate = self.get_delegate(delegate_name)
        result = delegate.on_pre_get_statistics(**kwargs)
        if isinstance(result, tornado.concurrent.Future):
            result = yield result
        raise Return(result)

    @coroutine
    def delegate_pre_get_list(self, delegate_name, **kwargs):
        delegate = self.get_delegate(delegate_name)
        result = delegate.on_pre_get_list(**kwargs)
        if isinstance(result, tornado.concurrent.Future):
            result = yield result
        raise Return(result)

    @coroutine
    def delegate_pre_create(self, delegate_name, **kwargs):
        delegate = self.get_delegate(delegate_name)
        specifics = delegate.on_pre_create(**kwargs)
        if isinstance(specifics, tornado.concurrent.Future):
            specifics = yield specifics
        raise Return(specifics)

    @coroutine
    def delegate_pre_get(self, delegate_name, **kwargs):
        delegate = self.get_delegate(delegate_name)
        result = delegate.on_pre_get(**kwargs)
        if isinstance(result, tornado.concurrent.Future):
            result = yield result
        raise Return(result)

    @coroutine
    def delegate_pre_update(self, delegate_name, **kwargs):
        delegate = self.get_delegate(delegate_name)
        result = delegate.on_pre_update(**kwargs)
        if isinstance(result, tornado.concurrent.Future):
            result = yield result
        raise Return(result)

    @coroutine
    def delegate_post_update(self, delegate_name, **kwargs):
        delegate = self.get_delegate(delegate_name)
        result = delegate.on_post_update(**kwargs)
        if isinstance(result, tornado.concurrent.Future):
            result = yield result
        raise Return(result)

    @coroutine
    def delegate_update(self, delegate_name, handle_update, data, **kwargs):
        delegate = self.get_delegate(delegate_name)
        specifics = delegate.on_pre_update(**kwargs)
        if isinstance(specifics, tornado.concurrent.Future):
            specifics = yield specifics
        if specifics is not None:
            data['specifics'] = specifics
        result = handle_update(data)
        if isinstance(result, tornado.concurrent.Future):
            result = yield result
        if result:
            post_result = delegate.on_post_update(**kwargs)
            if isinstance(post_result, tornado.concurrent.Future):
                yield post_result
        raise Return(result)

    def query_arguments(self):
        arguments = {}
        for key, value in self.request.query_arguments.iteritems():
            arguments[key] = value[len(value) - 1]
        return arguments

    @staticmethod
    def extract_fields(src, required_fields=None, optional_fields=None):
        return extract_fields(src, required_fields, optional_fields)

    @staticmethod
    def exclude_fields(src, exclusive_fields):
        exclude_fields(src, exclusive_fields)

    @property
    def redis_client(self):
        cls = self.__class__
        if cls._redis_client is None:
            cls._redis_client = tornadoredis.Client(connection_pool=CONNECTION_POOL)
            # cls._redis_client = redis.StrictRedis()
        return cls._redis_client

    @property
    def mqtornado(self):
        cls = self.__class__
        if cls._mqtornado is None:
            cls._mqtornado = Mqtonado()
            # tmp for debugging on macbook
            cls._mqtornado.connect(settings.MQ_HOST, username=settings.MQ_USER, password=settings.MQ_PASS)
        return cls._mqtornado

    def is_keys_required_present(self, keys_required, keys, equal=True):
        if equal:
            result = (set(keys_required) == set(keys))
        else:
            keys_required_set = set(keys_required)
            keys_set = set(keys)
            result = keys_required_set & keys_set == keys_required_set
        if not result:
            self.respond_error('keys_required_not_present', 'required key[s]: %s' % str(keys_required))
        return result

    def make_error_response(self, key, msg=None, error_code=None):
        if error_code is None:
            error_code = _error_codes[key]
        response = dict(result=False, error_code=error_code)
        error_message = ' '.join(key.split('_')) if msg is None else msg
        response.update(error_message=error_message)
        return response

    def respond_success(self, data=None):
        response = dict(result=True, error_code=0, error_message='ok')
        if data is not None:
            response.update(data=data)
        self.finish(response)

    def respond_error(self, key, msg=None):
        raise ErrorResponseException(key, msg)

    def _delegate_respond_error(self, error_code, msg=None):
        raise ErrorResponseException('delegate_error', msg, error_code)

    # def redirect_with_cookies(self, redirect_uri, result):
    #     urlparsed = urlparse.urlparse(redirect_uri)
    #     if 'access_token' in result:
    #         access_token = result['access_token']
    #         expires_at = result['expires_at']
    #         user_id = result['user_id']
    #         self.logger.info('urlparsed.netloc: %s, path: %s' % (urlparsed.netloc, self.request.path))
    #         self.set_cookie('access_token', access_token, expires=expires_at, domain=urlparsed.netloc)
    #         if '' == urlparsed.query:
    #             redirect_uri += '?user_id=%s' % user_id
    #         else:
    #             redirect_uri += '&user_id=%s' % user_id
    #     else:
    #         self.set_cookie('user_bind_info', base64.urlsafe_b64encode(json.dumps(result)), domain=urlparsed.netloc)
    #     self.redirect(redirect_uri)

    def gen_http_error(self, status, msg):
        exc = sys.exc_info()[1]
        if isinstance(exc, ErrorResponseException):
            self.logger.error('%s %s %s' % (exc.key, exc.msg, exc.error_code))
            response = self.make_error_response(exc.key, exc.msg, exc.error_code)
        else:
            logging.error(traceback.format_exc())
            response = self.make_error_response('invalid_request', traceback.format_exc())
        self.finish(response)

    def check_permissions(self, permissions):
        return True
        # import re
        # p = re.compile(r'/api/users/.+/password')
        # if p.match(self.request.path):
        #     return False
        # else:
        #     return True


class Wait(object):
    def __init__(self, on_complete, on_timeout, expires_in=5):
        self.on_complete = on_complete
        self.on_timeout = on_timeout
        self._expires_at = time.time() + expires_in

    @property
    def expires_at(self):
        return self._expires_at


class Waitings(object):
    _waitings = {}
    _periodic = None

    @classmethod
    def add(cls, key, wait):
        if cls._periodic is None:
            cls._periodic = PeriodicCallback(cls._check_timeout, 1000)
            cls._periodic.start()
        cls._waitings[key] = wait

    @classmethod
    def complete(cls, key, result):
        if cls._waitings.has_key(key):
            wait = cls._waitings[key]
            del cls._waitings[key]
            wait.on_complete(result)
            return True
        else:
            return False

    @classmethod
    def _check_timeout(cls):
        cur_time = time.time()
        expired_wait_keys = []
        for key, wait in cls._waitings.iteritems():
            if cur_time > wait.expires_at:
                expired_wait_keys.append(key)
                wait.on_timeout()
        for key in expired_wait_keys:
            del cls._waitings[key]


class Mqtonado(object):
    def __init__(self, on_message=None, userdata=None):
        self.on_message = on_message
        self.userdata = userdata
        self.waitings = Waitings()

    @staticmethod
    def _handle_misc(client):
        client.loop_misc()
        if client.want_write():
            IOLoop.current().update_handler(client.socket(), IOLoop.READ | IOLoop.WRITE | IOLoop.ERROR)

    @staticmethod
    def _handler(client, fd, events):
        if IOLoop.READ & events:
            client.loop_read()
        elif IOLoop.WRITE & events:
            client.loop_write()
            if not client.want_write():
                IOLoop.current().update_handler(fd, IOLoop.READ | IOLoop.ERROR)
        else:
            print 'io error'

    @staticmethod
    def _on_connect(client, userdata, flags, rc):
        logging.getLogger().info(("Connected with result code " + str(rc)))
        print("Connected with result code " + str(rc))
        # Subscribing in on_connect() means that if we lose the connection and
        # reconnect then subscriptions will be renewed.

    @staticmethod
    def _on_publish(client, userdata, mid):
        # print 'on_publish, mid: %d' % mid
        pass

    @staticmethod
    def _on_subscribe(client, userdata, mid, granted_qos):
        # print 'on_subscribe, mid: %d' % mid
        pass

    @staticmethod
    def _on_message(client, userdata, msg):
        self = userdata
        splited_topic = msg.topic.split('/')
        # NOTE: 如果这里加上 'f' 判断的话，会影响微信扫码登录后的通知
        # if 'f' == splited_topic[0] and 'p' in splited_topic and 'p' == splited_topic[-2]:
        if 'p' in splited_topic and 'p' == splited_topic[-2]:
            req_id = splited_topic[-1]
            self.waitings.complete(req_id, json.loads(msg.payload))
            self.unsubscribe(msg.topic)
        else:
            if self.on_message is not None:
                self.on_message(self, self.userdata, msg.topic, msg.payload)

    def connect(self, host, port=1883, keepalive=60, will_topic=None, will=None, username=None, password=None):
        self.client = mqtt.Client()
        self.client.user_data_set(self)
        self.client.on_connect = Mqtonado._on_connect
        self.client.on_message = Mqtonado._on_message
        self.client.on_publish = Mqtonado._on_publish
        self.client.on_subscribe = Mqtonado._on_subscribe
        if will_topic is not None:
            self.client.will_set(will_topic, will)
        if username is not None:
            self.client.username_pw_set(username, password)
        self.client.connect(host, port, keepalive)
        IOLoop.current().add_handler(self.client.socket(), functools.partial(Mqtonado._handler, self.client),
                                     IOLoop.READ | IOLoop.WRITE | IOLoop.ERROR)
        periodic = PeriodicCallback(functools.partial(Mqtonado._handle_misc, self.client), 5000)
        periodic.start()
        # IOLoop.current().start()

    def publish(self, topic, payload=None, callback=None):
        info = self.client.publish(topic, payload)
        IOLoop.current().update_handler(self.client.socket(), IOLoop.READ | IOLoop.WRITE | IOLoop.ERROR)
        # print 'mid: %d' % info[1]
        if callback is not None:
            pass

    def subscribe(self, topic, callback=None):
        info = self.client.subscribe(topic)
        IOLoop.current().update_handler(self.client.socket(), IOLoop.READ | IOLoop.WRITE | IOLoop.ERROR)
        # print 'mid: %d' % info[1]
        if callback is not None:
            pass

    def unsubscribe(self, topic, callback=None):
        info = self.client.unsubscribe(topic)
        IOLoop.current().update_handler(self.client.socket(), IOLoop.READ | IOLoop.WRITE | IOLoop.ERROR)
        # print 'mid: %d' % info[1]
        if callback is not None:
            pass

    def _on_timeout(self, topic_resp, on_timeout):
        self.unsubscribe(str(topic_resp))
        on_timeout()

    def wait_response_cancel(self, req_id):
        topic_resp = 'p/%s' % req_id
        self.unsubscribe(topic_resp)

    def wait_response_complete(self, req_id, payload=None):
        topic_resp = 'p/%s' % req_id
        self.publish(topic_resp, payload)

    def wait_response(self, req_id, wait):
        topic_resp = 'p/%s' % req_id
        self._wait_response_topic(req_id, wait, topic_resp)

    def _wait_response_topic(self, req_id, wait, topic_resp):
        on_timeout = wait.on_timeout
        wait.on_timeout = functools.partial(self._on_timeout, topic_resp, on_timeout)
        self.waitings.add(req_id, wait)
        self.subscribe(topic_resp)

    # NOTO: 必须保证在 publish 之前 subscribe 成功
    def _rpc(self, device_id, request_name, request, wait, to_device=True):
        resp = 'f'
        req = 't'
        if not to_device:
            resp = 't'
            req = 'f'
        req_id = str(ObjectId())
        topic_resp = '%s/d/%s/p/%s' % (resp, device_id, req_id)
        topic_req = '%s/d/%s/q/%s/%s' % (req, device_id, req_id, request_name)
        self._wait_response_topic(req_id, wait, topic_resp)
        if request is not None:
            self.publish(topic_req, json.dumps(request))
        else:
            self.publish(topic_req)

    def rpc_to_device(self, device_id, request_name, request, wait):
        self._rpc(device_id, request_name, request, wait, to_device=True)

    def rpc_from_device(self, device_id, request_name, request, wait):
        self._rpc(device_id, request_name, request, wait, to_device=False)


class PayBaseHandler(BaseHandler):
    def __init__(self, application, request, **kwargs):
        super(PayBaseHandler, self).__init__(application, request, **kwargs)

    @coroutine
    def set_order_paid(self, order, **modification):
        if 'paid' == order.status:
            raise Exception('call set_order_paid more than one time')

        products = order.products
        for product in products:
            pid = product['product_id']
            p = yield self.products_dao.get_record_by_id(pid)
            # TODO: 对于回调的 url，在 app setting 中设置安全域名
            if 'service' == p.type and p.notify_url is not None:
                body = p.specifics
                if 'specifics' in product:
                    if body is None:
                        body = product['specifics']
                    else:
                        body.update(product['specifics'])
                body = json.dumps(body)
                self.logger.info(body)
                http_request = HTTPRequest(p.notify_url, method='POST', body=body)
                http_client = AsyncHTTPClient()
                response = yield http_client.fetch(http_request, raise_error=False)
                self.logger.info(response.body)

        oid = order.id_str
        ts = int(time.time())
        if 'goods' == order.type:
            modification.update(status='paid', paid_time=ts)
        else:
            modification.update(status='done', paid_time=ts, done_time=ts)
        kwargs = dict(oid=oid, modification=modification)
        handle_update = functools.partial(self.orders_dao.pay_update_order, oid)
        result = yield self.delegate_update('orders', handle_update, modification, **kwargs)
        raise Return(result)
